package tp1;

import java.io.*;
import java.net.*;
import java.util.*;

import static java.lang.Thread.sleep;

// Classe client principale.

public final class Client {

    public static final String ANSI_RESET = "\u001B[0m";
    public static final String ANSI_BLACK = "\u001B[30m";
    public static final String ANSI_RED = "\u001B[31m";
    public static final String ANSI_GREEN = "\u001B[32m";
    public static final String ANSI_YELLOW = "\u001B[33m";
    public static final String ANSI_BLUE = "\u001B[34m";
    public static final String ANSI_PURPLE = "\u001B[35m";
    public static final String ANSI_CYAN = "\u001B[36m";
    public static final String ANSI_WHITE = "\u001B[37m";

    // Liste de groupes et utilisaturs disponibles
    public static HashSet<Utilisateur> utilisateurs = new HashSet<Utilisateur>();
    public static HashSet<Groupe> groupes = new HashSet<Groupe>();

    protected static Utilisateur usrCourant = null;
    protected static BufferedReader reception = null;
    private static volatile Client instance = null;
    private static Scanner inFromUser;

    public static InetAddress adresseLocale = null; // Adresse de cette machine
    public static boolean modeDebug = false;
    private static String version = "32a";
    private static final int portTCP=1027;
    private static final int portServeurUDP=7650;
    public static final String adresseMulticast="224.5.6.7"; // adresse multicast pour la communication en groupe

    /**
     * Constructeur de l'objet.
     */
    private Client() {
        // La présence d'un constructeur privé supprime le constructeur public par défaut.
        // De plus, seul le singleton peut s'instancier lui-même.
        super();
    }

    /**
     * Méthode permettant de renvoyer une instance de la classe Client
     * @return Retourne l'instance du singleton.
     */
    public final static Client getInstance() {
        //Le "Double-Checked Client"/"Client doublement vérifié" permet
        //d'éviter un appel coûteux à synchronized,
        //une fois que l'instanciation est faite.
        if (Client.instance == null) {
            // Le mot-clé synchronized sur ce bloc empêche toute instanciation
            // multiple même par différents "threads".
            // Il est TRES important.
            synchronized(Client.class) {
                if (Client.instance == null) {
                    Client.instance = new Client();
                }
            }
        }
        return Client.instance;
    }

    private static void print_utilisateurs() {
        // Affichage de la liste des utilisateurs
        System.out.println("Il y a "+utilisateurs.size()+" utilisateurs connectés.");
        Iterator<Utilisateur> it = utilisateurs.iterator();
        String nom,port;
        String[] adresse;
        Utilisateur u;
        int count=0;

        System.out.println("=====================================");
        System.out.println("Liste des utilisateurs : ");
        System.out.println("Id.       | Adresse       |Port ");
        System.out.println("----------+---------------+-----");
        while(it.hasNext()) {
            count++;
            u = it.next();
            nom = String.format("%-10s",u.getIdentifiant());
            port = String.format("%05d",u.getPort()); //formatte le port sur 5 caractères
            adresse = u.getAdresse().getHostAddress().split("\\.");
            for(int i=0;i<adresse.length;i++) {
                adresse[i] = String.format("%03d", Integer.parseInt(adresse[i])); //formatte l'adresse sous la forme 000.000.000.000
            }
            System.out.println(nom+"|"+adresse[0]+"."+adresse[1]+"."+adresse[2]+"."+adresse[3]+"|"+port);
        }
        System.out.println("=====================================");
    }

    private static void print_groupes() {
        //Affichage de la liste de groupes
        String nom, port;
        String[] adresse;

        Iterator<Groupe> itg = groupes.iterator();
        Groupe g;
        int count = 0;

        System.out.println("=====================================");
        System.out.println("Liste des groupes : ");
        while (itg.hasNext()) {
            count++;
            g = itg.next();
            nom = String.format("%-6s", g.getNom());
            port = String.format("%05d", g.getPort()); //formatte le port sur 5 caractères
            adresse = g.getAdresseMulticast().getHostAddress().split("\\.");
            System.out.println("Groupe n°" + count + " Nom : " + nom + " Port : " + port);
            for (int i = 0; i < adresse.length; i++) {
                adresse[i] = String.format("%03d", Integer.parseInt(adresse[i])); //formatte l'adresse sous la forme 000.000.000.000
            }
        }
        System.out.println("=====================================");
    }

    private static void ferme_connexions(PrintWriter emission, InputStreamReader in_reader, Socket c_socket,int status) {
        debug("ferme_connexions()");
        try {
            emission.println("END");
            emission.flush();
            emission.close();
            in_reader.close();
            c_socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.exit(status);
    }

    private static InetAddress getAdresseLocale(Scanner clavier) throws IOException {

        Enumeration<NetworkInterface> enumReseau = null;
        enumReseau = NetworkInterface.getNetworkInterfaces();
        NetworkInterface interfaceReseau = null;
        InetAddress adresseMachine = null;
        debug("****Liste des Adresses réseau ****\n\n");

        for (NetworkInterface nint : Collections.list(enumReseau)) {

            debug("Nom : "+ nint.getDisplayName()+"\t Adresse Loopback ? "+nint.isLoopback());
            //System.out.println(nint.getName());
            //  System.out.println(nint.isLoopback());
            //  System.out.println(nint.isPointToPoint());
            debug("\nAdresses réseau :");
            Enumeration<InetAddress> inetAddresses = nint.getInetAddresses();
            for (InetAddress inetAddress : Collections.list(inetAddresses)) {
                debug("Adresse : "+inetAddress.getCanonicalHostName()+"\t On site ? "+inetAddress.isSiteLocalAddress());
                if(inetAddress.isSiteLocalAddress() && !inetAddress.isLoopbackAddress())
                    adresseMachine = InetAddress.getByName(inetAddress.getHostName());
            }
        }

        System.out.println(ANSI_GREEN+"Adresse machine choisie : "+ANSI_RESET+adresseMachine.getHostAddress());

        if(modeDebug) {
            System.out.println("Appuyez sue ENTREE pour continuer ou saisissez une adresse réseau différente");
            String nouvelleAdresse = clavier.nextLine();

            if (nouvelleAdresse.length() > 0) {
                try {
                    InetAddress address = InetAddress.getByName(nouvelleAdresse);
                    if (address.isReachable(200))
                        adresseMachine = address;
                    else
                        System.out.println(ANSI_RED + "ERREUR:" + ANSI_RESET + "Impossible de se connecter à l'adresse " + address.toString());
                } catch (UnknownHostException e) {
                    System.out.println("Impossible de se connecter à l'adresse spécifiée. ENTREE pour continuer.");
                    clavier.nextLine();
                }
            }
        }
        return(adresseMachine);
    }

    protected static void debug(String message) {
        String couleur = ANSI_RED;
        if(modeDebug) {
            System.out.println(couleur+"[Message Debug] "+message+ANSI_RESET);
        }
    }

    private static void print_menu() {

        System.out.println(ANSI_PURPLE+"Commandes disponibles :\n");
        System.out.println("\tLUT : liste des utilisateurs connectés");
        System.out.println("\tLGR : liste des groupes de discussion");
        System.out.println("\tCHAT nom_utilisateur : commencer un chat avec l'utilisateur");
        System.out.println("\tCGR nom_groupe : créer un groupe de discussion");
        System.out.println("\tRGR nom_groupe : rejoindre un groupe de discussion\n");

        System.out.println("\tEND : terminer l'application et sortir."+ANSI_RESET);
    }

    private static InetAddress getAdresseServeur() throws InterruptedException, IOException {
        MulticastSocket multSocket = new MulticastSocket(portServeurUDP);
        multSocket.joinGroup(InetAddress.getByName(adresseMulticast));
        byte[] receiveData = null;
        int count = 0;
        InetAddress adresseServeur=null;

        while (count < 10) {
            receiveData = new byte[1024];
            DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
            try {
                multSocket.receive(receivePacket);
                adresseServeur = receivePacket.getAddress();
                return adresseServeur;
            } catch (IOException e) {
                e.printStackTrace();
            }
            sleep(1000);
            count++;
        }
        return adresseServeur;
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        Socket c_socket = null;
        MulticastSocket multSocket = null;
        DatagramSocket udp_socket=null;
        // flux de lecture ecriture
        PrintWriter emission = null;
        InputStream in = null;
        InputStreamReader in_reader = null;
        Scanner	cmd = new Scanner(System.in);
        int udpPort=-1;
        //InetAddress adresseMachine=null;

        boolean continuer = true;
        boolean continuerTCP = true;
        boolean continuerUDP = true;
        boolean continuerChat = false;
        boolean conversation = false;
        boolean id = false;

        byte[] msg = new byte[1024];
        String sEnvoi = null, sReception = null;
        String identifiant = null;
        ClientLecture threadLecture = null;
        Groupe groupe = null;

        // UDP Multicast
        ClientUDP multReception = null;
        byte[] multSendData = null;
        DatagramPacket multSendPacket = null;

        System.out.println("Version N°"+version);
        if(args.length>0) {
            for(int i=0;i<args.length;i++) {
                System.out.println("Arg[" + i + "] = " + args[i]);
                if (args[i].equals("-d") || args[i].equals("--debug")) {
                    modeDebug = true;
                    System.out.println(ANSI_RED+"================Mode Debug========================="+ANSI_RESET);
                }
            }
        }

        adresseLocale = getAdresseLocale(cmd);

        if(adresseLocale==null) {
            System.out.println("Impossible de trouver l'adresse locale. Failure.");
            System.exit(1);
        }

        usrCourant = new Utilisateur("",adresseLocale,portTCP);

        InetAddress adresseServeur;
        adresseServeur =  getAdresseServeur();
        String adresseServeurName=null;
        if(adresseServeur==null) {
            System.out.println("Aucune annonce de serveur sur le réseau à l'adresse : "+adresseMulticast+" port "+portServeurUDP);
            System.out.println("Saisir l'adresse du serveur auquel vous voulez vous connecter [ou ENTREE pour localhost par defaut]:");
            adresseServeurName = cmd.nextLine();
            adresseServeur = InetAddress.getByName(adresseServeurName);
        } else {
            System.out.println(ANSI_GREEN+"Serveur trouvé à l'adresse : "+ANSI_RESET+adresseServeur.getHostAddress());
        }

        try {

            c_socket = new Socket(adresseServeur,1027);
            debug("J'essaye de me connecter au serveur à l'adresse " + adresseServeur.getHostName());
            udp_socket = new DatagramSocket();
            debug("DatagramSocket addr" + udp_socket.getLocalAddress() + " port " + udp_socket.getLocalPort());
            udpPort = udp_socket.getLocalPort();
            usrCourant.setPort(udpPort);
            in = c_socket.getInputStream();
            in_reader = new InputStreamReader(in);
            emission = new PrintWriter(new OutputStreamWriter(c_socket.getOutputStream()));
            reception = new BufferedReader(in_reader);
            //lance le serveur UDP pour les chats
            Thread schat = new ClientChat(udp_socket);
            schat.start();

        } catch (IOException e) {
            System.out.println("Impossible de se connecter au serveur.");
            if(modeDebug)
                e.printStackTrace();
            System.exit(1);
        }

        try {
            while(continuer){
                // partie TCP avec serveur
                while(continuerTCP){
                    //Identification avec le serveur
                    int count = 0;

                    while(!id) {
                        System.out.printf("Saisir votre identifiant : ");
                        identifiant = cmd.nextLine();
                        identifiant = String.format("%-6s",identifiant).substring(0,6); // formatte l'identifiant sur 6 caractères
                        sEnvoi=identifiant+String.format("%05d",udpPort);
                        emission.println("IDT"+sEnvoi);
                        emission.flush();
                        sReception = reception.readLine();
                        debug("[Client] Message serveur : " + sReception);
                        if(sReception.equalsIgnoreCase("IDO")) {
                            System.out.println(ANSI_GREEN+"Authentification réussie. Vous êtes connecté avec l'identifiant : "+ANSI_RESET+identifiant);
                            usrCourant.setIdentifiant(identifiant);
                            id = true;
                            // lance le thread de lecture du serveur
                            threadLecture = new ClientLecture(c_socket);
                            threadLecture.start();
                        } else {
                            System.out.println("Authentification échouée");
                            count++;
                            if(count>=3) {
                                System.out.println("Abandon.");
                                ferme_connexions(emission,in_reader,c_socket,1);
                            }
                        }
                    }
                    print_menu();
                    System.out.printf("Entrez votre commande : ");
                    sEnvoi = cmd.nextLine();
                    System.out.println(ANSI_GREEN+"[Commande reçue : " + sEnvoi+"]"+ANSI_RESET);
                    msg = sEnvoi.getBytes();

                    if(sEnvoi.equalsIgnoreCase("end")){
                        continuer = false;
                        continuerTCP = false;
                        continuerUDP = false;
                        threadLecture.arreter();
                        System.out.println("Application terminée à la demande de l'utilisateur");
                        // emission.println(sEnvoi);
                        //emission.flush();
                        // ferme les connexions et sort
                        ferme_connexions(emission,in_reader,c_socket,1);
                    }

                    if(sEnvoi.equalsIgnoreCase("lut")){
                        print_utilisateurs();
                    }
                    if(sEnvoi.equalsIgnoreCase("lgr")){
                        print_groupes();
                    }


                    if (sEnvoi.length() > 3 && sEnvoi.substring(0, 4).equalsIgnoreCase("chat")) {
                        System.out.println(ANSI_BLUE+"Mode chat."+ANSI_RESET);
                        continuerChat = true;
                        String utilisateur = String.format("%-6s", sEnvoi.substring(5));
                        debug("Essaye d'établir unchat avec " + utilisateur);
                        Iterator<Utilisateur> it = utilisateurs.iterator();
                        Utilisateur u = null;
                        boolean trouve = false;

                        while (it.hasNext()) {
                            u = it.next();
                            if (u.getIdentifiant().equals(utilisateur)) {
                                trouve = true;
                                debug("Trouvé " + utilisateur + "=" + u.getIdentifiant());
                                break;
                            }
                        }

                        if (!trouve) {
                            System.out.println("Utilisateur non trouvé");
                            trouve = false;
                            continuerChat = false;
                        }

                        while (continuerChat) {
                            String couleurChat=ANSI_BLUE;
                            conversation = true;
                            byte[] messageEnvoyer = new byte[1024];
                            DatagramSocket sendSocket = new DatagramSocket(); // choisit un port au hasard

                            System.out.println("Veuillez entrer un message. TCP pour revenir en mode TCP, END pour quitter.");
                            inFromUser = new Scanner(System.in);
                            int nMsg = 0; // N° MSG : numérotation des messages du chat
                            while (conversation) {

                                System.out.printf(couleurChat + "[Texte à envoyer à " + ANSI_RESET +u.getIdentifiant() +couleurChat+" :] " + ANSI_RESET);
                                String sentence = inFromUser.nextLine();
                                nMsg++;

                                if (sentence.length() > 2 && sentence.substring(0, 3).equalsIgnoreCase("END")) {
                                    conversation = false;
                                    continuerChat = false;
                                    continuer = false;
                                    continuerUDP = false;
                              //      sendSocket.close();
                                    ferme_connexions(emission, in_reader, c_socket, 1);
                                    System.out.println("Au revoir.");
                                } else if (sentence.length() > 2 && sentence.substring(0, 3).equalsIgnoreCase("TCP")) {
                                    conversation = false;
                                    continuerChat = false;
                                    continuerTCP = true;
                                    continuerUDP = false;
                                    sendSocket.close();
                                    System.out.println("Retour en mode TCP.");
                                    //TODO : problème retour en mode TCP, il rejoint à nouveau le groupe automatiquement
                                } else {
                                    debug("Message envoyé à l'utilisateur " + u.getIdentifiant() + "port " + u.getPort() + "addr " + u.getAdresse());
                                    sentence = "MSG"+String.format("%02d",nMsg)+identifiant+sentence;
                                    messageEnvoyer = sentence.getBytes();
                                    DatagramPacket sendPacket = new
                                            DatagramPacket(messageEnvoyer, messageEnvoyer.length, u.getAdresse(), u.getPort());
                                    sendSocket.send(sendPacket);
                                }
                            }
                        }
                    }

                    String[] messagesInternes={"lut","lgr","cha","tcp"};
                    if (! Arrays.asList(messagesInternes).contains(sEnvoi.substring(0,3).toLowerCase()) ) {
                        // Si ce n'est pas une commande interne, envoyer le message au serveur
                        emission.println(sEnvoi);
                        emission.flush();
                        sReception = reception.readLine();
                        debug("Serveur : " + sReception);
                    }

                    if(threadLecture.groupeRejoint){
                        System.out.println("Groupe rejoint !");
                        System.out.println("Coords du groupe : "+threadLecture.coordGroupe[1]);

                        groupe = new Groupe(threadLecture.coordGroupe[1],
                                InetAddress.getByName(threadLecture.coordGroupe[2]),
                                Integer.parseInt(threadLecture.coordGroupe[3]));
                        System.out.println("Groupe rejoint : " +groupe.getNom()+" "
                                +groupe.getAdresseMulticast().getHostAddress()+" "+groupe.getPort());
                        continuerTCP = false;
                        continuerUDP = true;
                    }

                    if(sReception.length() > 2 && sReception.substring(0,3).equalsIgnoreCase("rgo")) {
                        continuerTCP = false;
                        continuerUDP = true;
                        String nomGroupe = sReception.substring(3,13);
                        String adresseGroupe = sReception.substring(13,28);
                        String portGroupe = sReception.substring(28,33);
                        groupe = new Groupe(nomGroupe,
                                InetAddress.getByName(adresseGroupe),
                                Integer.parseInt(portGroupe));
                        System.out.println("Groupe rejoint : " +groupe.getNom()+" "
                                +groupe.getAdresseMulticast().getHostAddress()+" "+groupe.getPort());
                    }

                    if(sReception.length() > 2 && sReception.substring(0,3).equalsIgnoreCase("RGK")) {
                        System.out.println("Le groupe nexiste pas, veuillez entrer un groupe valide.");
                    }

                }

                if(continuerUDP)
                {
                    debug("On entre en mode UDP");
                    // envoi automatique de HLO
                    multSocket = new MulticastSocket(groupe.port);
                    multReception = new ClientUDP(groupe, multSocket, usrCourant);
                    multReception.start();
                    debug("Thread de lecture lancé");
                    String msgMult = "HLO"+"00"+usrCourant.getIdentifiant();
                    multReception.setDernierMsg(msgMult);
                    sleep(100); //donne du tps pour l'arrivée des messages
                    multSendData = new byte[1024];
                    multSendData = msgMult.getBytes();
                    DatagramPacket sendPacket = new DatagramPacket(multSendData,
                            multSendData.length, groupe.adresseMulticast, groupe.port);
                    multSocket.send(sendPacket);

                    System.out.println("Veuillez entrer un message. TCP pour revenir en mode TCP, END pour quitter.");

                    int nMsg=0;
                    String couleurChat = ANSI_BLUE;
                    while(continuerUDP){
                        System.out.printf(couleurChat + "Message à envoyer : " + ANSI_RESET);
                        inFromUser = new Scanner(System.in);
                        nMsg++;
                        multSendData = new byte[1024];
                        String texte = inFromUser.nextLine();
                        msgMult="MSG"+String.format("%02d",nMsg)+identifiant+texte;
                        if(texte.length()>2 && texte.substring(0, 3).equalsIgnoreCase("END")){
                            continuerUDP = false;
                            continuer = false;
                            threadLecture.groupeRejoint = false; 
                            System.out.println("Au revoir UDP.");
                           // multReception.arreter();
                            ferme_connexions(emission,in_reader,c_socket,1);
                        } else if (texte.length() > 2 && texte.substring(0, 3).equalsIgnoreCase("TCP")) {
                            continuerUDP = false;
                            continuerTCP = true;
                            threadLecture.groupeRejoint = false; 
                            System.out.println("Retour en mode TCP.");
                            sReception = " ";
                            multReception.arreter();
                        } else {
                            multReception.setDernierMsg(msgMult);
                            multSendData = msgMult.getBytes();
                            sendPacket = new
                                    DatagramPacket(multSendData, multSendData.length, groupe.adresseMulticast, groupe.port);
                            multSocket.send(sendPacket);
                        }
                    }
                }
                debug("Continuer = "+continuer);
            }

        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();

            System.out.println("Exception pendant la communication. Le serveur a probablement fermé la connexion");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            debug("finally");
            ferme_connexions(emission,in_reader,c_socket,1);

        }

    }



}
